/*
 * DialogAboutGLLogoCanvas
 * Version: 0.1
 * Date: 2008-10-25
 * Author: gp08-nhn, Pepijn Van Eeckhoudt, nehe, jogl
 */

package keyboardhero;

import java.awt.image.*;
import java.io.*;
import java.nio.*;
import javax.media.opengl.*;
import javax.media.opengl.glu.*;
import javax.imageio.*;
import com.sun.opengl.util.*;

/**
 * The GL canvas for the rotating 3D cube logo. Based on Pepijn Van Eeckhoudt's NEHE blending
 * lesson.
 * 
 * @author Pepijn Van Eeckhoudt, Daniel Nicholas Kiss
 */
final class DialogAboutGLLogoCanvas extends GLCanvas implements GLEventListener {
	
	//-------------------- NESTED CLASSES --------------------
	
	private final static class TextureReader {
		public static Texture readTexture(String filename) throws IOException {
			return readTexture(filename, false);
		}
		
		public static Texture readTexture(String filename, boolean storeAlphaChannel) throws IOException {
			BufferedImage bufferedImage;
			bufferedImage = readImage(filename);
			return readPixels(bufferedImage, storeAlphaChannel);
		}
		
		private static BufferedImage readImage(String resourceName) throws IOException {
			return ImageIO.read(Util.getURL(resourceName));
		}
		
		private static Texture readPixels(BufferedImage img, boolean storeAlphaChannel) {
			int[] packedPixels = new int[img.getWidth() * img.getHeight()];
			
			PixelGrabber pixelgrabber = new PixelGrabber(img, 0, 0, img.getWidth(), img.getHeight(), packedPixels, 0, img.getWidth());
			try {
				pixelgrabber.grabPixels();
			} catch(InterruptedException e) {
				throw new RuntimeException();
			}
			
			int bytesPerPixel = storeAlphaChannel ? 4 : 3;
			ByteBuffer unpackedPixels = BufferUtil.newByteBuffer(packedPixels.length * bytesPerPixel);
			
			for(int row = img.getHeight() - 1; row >= 0; row--) {
				for(int col = 0; col < img.getWidth(); col++) {
					int packedPixel = packedPixels[row * img.getWidth() + col];
					unpackedPixels.put((byte)((packedPixel >> 16) & 0xFF));
					unpackedPixels.put((byte)((packedPixel >> 8) & 0xFF));
					unpackedPixels.put((byte)((packedPixel >> 0) & 0xFF));
					
					if(storeAlphaChannel) {
						unpackedPixels.put((byte)((packedPixel >> 24) & 0xFF));
					}
				}
			}
			
			unpackedPixels.flip();
			return new Texture(unpackedPixels, img.getWidth(), img.getHeight());
		}
		
		public static class Texture {
			private ByteBuffer pixels;
			private int width;
			private int height;
			
			public Texture(ByteBuffer pixels, int width, int height) {
				this.height = height;
				this.pixels = pixels;
				this.width = width;
			}
			
			public int getHeight() {
				return height;
			}
			
			public ByteBuffer getPixels() {
				return pixels;
			}
			
			public int getWidth() {
				return width;
			}
		}
	}
	
	//-------------------- VARIABLES --------------------
	
	// ××××× SETTINGS VARIABLES ×××××
	private static final int FPS = 20;
	private static final int UPDATE_TIME = 20;
	
	private static final float XSPEED = 0.8f; // X Rotation Speed
	private static final float YSPEED = 0.4f; // Y Rotation Speed
	private static final float ZSPEED = 0.2f; // Z Rotation Speed
	
	private static final float ZMIN = -15.f;
	private static final float ZMAX = -1f;
	private static final float ZMOVEMENT_SPEED = 0.05f;
	
	private static final float[] LIGHT_AMBIENT = {0.5f, 0.5f, 0.5f, 1.0f};
	private static final float[] LIGHT_DIFFUSE = {1.0f, 1.0f, 1.0f, 1.0f};
	private static final float[] LIGHT_POSITION = {0.0f, 0.0f, 2.0f, 1.0f};
	
	// ××××× CORE VARIABLES ×××××
	/**
	 * A GLCanvas is a serializable object, thus it requires a unique Serial Version ID.
	 * 
	 * @see java.io.Serializable
	 */
	private static final long serialVersionUID = -4017287613759893236L;
	
	private final GLU glu = new GLU();
	private final FPSAnimator fpsAnimator;
	private final Thread thread;
	private boolean threadRun;
	
	// ××××× ANIMATION VARIABLES ×××××
	private float xrot; // X Rotation
	private float yrot; // Y Rotation
	private float zrot; // Z Rotation
	private float z = ZMIN; // Depth Into The Screen
	private float zmovement = ZMOVEMENT_SPEED;
	
	//-------------------- CORE FUNCTIONS --------------------
	
	DialogAboutGLLogoCanvas(int width, int height) {
		super();
		setSize(width, height);
		setIgnoreRepaint(false);
		addGLEventListener(this);
		
		threadRun = true;
		(thread = new Thread() {
			public void run() {
				while(threadRun) {
					update();
					try {
						Thread.sleep(UPDATE_TIME);
					} catch(InterruptedException e) {}
				}
			}
		}).start();
		
		(fpsAnimator = new FPSAnimator(this, FPS)).start();
	}
	
	void close() {
		fpsAnimator.stop();
		threadRun = false;
		thread.interrupt();
	}
	
	//-------------------- GL FUNCTIONS --------------------
	
	public void displayChanged(GLAutoDrawable glDrawable, boolean modeChanged, boolean deviceChanged) {
	}
	
	/* void reshape(int width, int height) Called after the first paint command. */
	public void reshape(GLAutoDrawable glDrawable, int x, int y, int w, int h) {
		final GL gl = glDrawable.getGL();
		
		if(h <= 0) h = 1; // avoid a division by zero error!
		final float a = (float)w / (float)h;
		gl.glViewport(0, 0, w, h);
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(45.0f, a, 1.0, 20.0);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity(); // Reset The ModalView Matrix
	}
	
	/* void init() Called just AFTER the GL-Context is created. */
	public void init(GLAutoDrawable glDrawable) {
		GL gl = glDrawable.getGL();
		
		try {
			TextureReader.Texture texture = TextureReader.readTexture("GLLogo.png");
			gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
			gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_NEAREST);
			glu.gluBuild2DMipmaps(GL.GL_TEXTURE_2D, 3, texture.getWidth(), texture.getHeight(), GL.GL_RGB, GL.GL_UNSIGNED_BYTE, texture.getPixels());
		} catch(IOException e) {
			Util.debug(67, "GLLogo: Unable to load textures!");
			throw new RuntimeException(e);
		}
		
		gl.glEnable(GL.GL_TEXTURE_2D); // Enable Texture Mapping
		gl.glShadeModel(GL.GL_SMOOTH); //Enables Smooth Color Shading
		gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //This Will Clear The Background Color To Black
		gl.glClearDepth(1.0); //Enables Clearing Of The Depth Buffer
		gl.glEnable(GL.GL_DEPTH_TEST); //Enables Depth Testing
		gl.glDepthFunc(GL.GL_LEQUAL); //The Type Of Depth Test To Do
		gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST); // Really Nice Perspective Calculations
		gl.glLightfv(GL.GL_LIGHT1, GL.GL_AMBIENT, LIGHT_AMBIENT, 0); // Setup The Ambient Light
		gl.glLightfv(GL.GL_LIGHT1, GL.GL_DIFFUSE, LIGHT_DIFFUSE, 0); // Setup The Diffuse Light
		gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, LIGHT_POSITION, 0); // Position The Light
		gl.glEnable(GL.GL_LIGHT1); // Enable Light One
		gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f); // Full Brightness.  50% Alpha (new )
		gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE); // Set The Blending Function For Translucency (new )
		gl.glEnable(GL.GL_BLEND); // Turn Blending On
		gl.glDisable(GL.GL_DEPTH_TEST); // Turn Depth Testing Off
		gl.glEnable(GL.GL_LIGHTING);
	}
	
	private void drawGLScene(GLAutoDrawable glDrawable) {
		final GL gl = glDrawable.getGL();
		
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); //Clear The Screen And The Depth Buffer
		
		gl.glLoadIdentity(); //Reset The View
		gl.glTranslatef(0.0f, 0.0f, z);
		
		gl.glRotatef(xrot, 1.0f, 0.0f, 0.0f);
		gl.glRotatef(yrot, 0.0f, 1.0f, 0.0f);
		gl.glRotatef(zrot, 0.0f, 0.0f, 1.0f);
		
		gl.glBegin(GL.GL_QUADS);
		// Front Face
		gl.glNormal3f(0.0f, 0.0f, 1.0f);
		gl.glTexCoord2f(0.0f, 0.0f);
		gl.glVertex3f(-1.0f, -1.0f, 1.0f);
		gl.glTexCoord2f(1.0f, 0.0f);
		gl.glVertex3f(1.0f, -1.0f, 1.0f);
		gl.glTexCoord2f(1.0f, 1.0f);
		gl.glVertex3f(1.0f, 1.0f, 1.0f);
		gl.glTexCoord2f(0.0f, 1.0f);
		gl.glVertex3f(-1.0f, 1.0f, 1.0f);
		// Back Face
		gl.glNormal3f(0.0f, 0.0f, -1.0f);
		gl.glTexCoord2f(1.0f, 0.0f);
		gl.glVertex3f(-1.0f, -1.0f, -1.0f);
		gl.glTexCoord2f(1.0f, 1.0f);
		gl.glVertex3f(-1.0f, 1.0f, -1.0f);
		gl.glTexCoord2f(0.0f, 1.0f);
		gl.glVertex3f(1.0f, 1.0f, -1.0f);
		gl.glTexCoord2f(0.0f, 0.0f);
		gl.glVertex3f(1.0f, -1.0f, -1.0f);
		// Top Face
		gl.glNormal3f(0.0f, 1.0f, 0.0f);
		gl.glTexCoord2f(0.0f, 1.0f);
		gl.glVertex3f(-1.0f, 1.0f, -1.0f);
		gl.glTexCoord2f(0.0f, 0.0f);
		gl.glVertex3f(-1.0f, 1.0f, 1.0f);
		gl.glTexCoord2f(1.0f, 0.0f);
		gl.glVertex3f(1.0f, 1.0f, 1.0f);
		gl.glTexCoord2f(1.0f, 1.0f);
		gl.glVertex3f(1.0f, 1.0f, -1.0f);
		// Bottom Face
		gl.glNormal3f(0.0f, -1.0f, 0.0f);
		gl.glTexCoord2f(1.0f, 1.0f);
		gl.glVertex3f(-1.0f, -1.0f, -1.0f);
		gl.glTexCoord2f(0.0f, 1.0f);
		gl.glVertex3f(1.0f, -1.0f, -1.0f);
		gl.glTexCoord2f(0.0f, 0.0f);
		gl.glVertex3f(1.0f, -1.0f, 1.0f);
		gl.glTexCoord2f(1.0f, 0.0f);
		gl.glVertex3f(-1.0f, -1.0f, 1.0f);
		// Right face
		gl.glNormal3f(1.0f, 0.0f, 0.0f);
		gl.glTexCoord2f(1.0f, 0.0f);
		gl.glVertex3f(1.0f, -1.0f, -1.0f);
		gl.glTexCoord2f(1.0f, 1.0f);
		gl.glVertex3f(1.0f, 1.0f, -1.0f);
		gl.glTexCoord2f(0.0f, 1.0f);
		gl.glVertex3f(1.0f, 1.0f, 1.0f);
		gl.glTexCoord2f(0.0f, 0.0f);
		gl.glVertex3f(1.0f, -1.0f, 1.0f);
		// Left Face
		gl.glNormal3f(-1.0f, 0.0f, 0.0f);
		gl.glTexCoord2f(0.0f, 0.0f);
		gl.glVertex3f(-1.0f, -1.0f, -1.0f);
		gl.glTexCoord2f(1.0f, 0.0f);
		gl.glVertex3f(-1.0f, -1.0f, 1.0f);
		gl.glTexCoord2f(1.0f, 1.0f);
		gl.glVertex3f(-1.0f, 1.0f, 1.0f);
		gl.glTexCoord2f(0.0f, 1.0f);
		gl.glVertex3f(-1.0f, 1.0f, -1.0f);
		gl.glEnd();
	}
	
	public void display(GLAutoDrawable glDrawable) {
		drawGLScene(glDrawable);
	}
	
	private void update() {
		xrot += XSPEED;
		yrot += YSPEED;
		zrot += ZSPEED;
		z += zmovement;
		if(z > ZMAX && zmovement > 0) zmovement *= -1;
		else if(z < ZMIN && zmovement < 0) zmovement *= -1;
	}
	

	//-------------------- DEBUGGING FUNCTIONS --------------------
	
	/**
	 * Creates a string containing the most important information about the game. This method is
	 * used only for debugging and testing purposes.
	 * 
	 * @return the created string.
	 */
	static String getString() { //$SUP-MCP$
		return "DialogAboutGLLogoCanvas()";
	} // end method
	
	/**
	 * This method serves security purposes. Provides an integrity string that will be checked by
	 * the {@link Connection#integrityCheck()} method; thus the application can only be altered if
	 * the source is known. Every class in the {@link keyboardhero} package has an integrity string.
	 * 
	 * @return the string of this class used for integrity checking.
	 */
	static String getIntegrityString() {
		return "4t5ASKDFJLdfäđĐfg";
	} // end method
	
	/**
	 * The tester object of this class. It provides a debugging menu and unit tests for this class.
	 * Its only purpose is debugging or testing.
	 */
	static final Tester tester = new Tester("DialogAboutGLLogoCanvas", new String[] {"getString()"} /* end initialization */) { //$SUP-PPA$
		void menu(int choice) throws Exception {
			switch(choice) {
				case 5:
					System.out.println(getString());
					break;
				default:
					baseMenu(choice);
					break;
			} // end switch
		} // end method
		
		void runUnitTests() throws Exception {
			higherTestStart(name);
			
			testEq("getIntegrityString()", "4t5ASKDFJLdfäđĐfg", DialogAboutGLLogo.getIntegrityString());
			
			higherTestEnd();
		} // end method
	}; // end class
	
	/**
	 * Starts the class's developing menu. If this build is a developer's one it starts the
	 * application in a normal way with the exception that it starts the debugging tool for this
	 * class as well; otherwise exits with an error message.
	 * 
	 * @param args
	 *            the arguments given to the program.
	 * @see KeyboardHero#startApp()
	 */
	public static void main(String[] args) {
		Tester.mainer(args, tester);
	} // end method
}
